/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.drawee.view;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;

import com.facebook.common.activitylistener.ActivityListener;
import com.facebook.common.activitylistener.BaseActivityListener;
import com.facebook.common.activitylistener.ListenableActivity;
import com.facebook.common.internal.Objects;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.logging.FLog;
import com.facebook.drawee.components.DraweeEventTracker;
import com.facebook.drawee.drawable.VisibilityAwareDrawable;
import com.facebook.drawee.drawable.VisibilityCallback;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.interfaces.DraweeHierarchy;

import javax.annotation.Nullable;

import static com.facebook.drawee.components.DraweeEventTracker.Event;

/**
 * A holder class for Drawee controller and hierarchy.
 * <p/>
 * <p>Drawee users, should, as a rule, use {@link DraweeView} or its subclasses. There are
 * situations where custom views are required, however, and this class is for those circumstances.
 * <p/>
 * <p>Each {@link DraweeHierarchy} object should be contained in a single instance of this
 * class.
 * <p/>
 * <p>Users of this class must call {@link Drawable#setBounds} on the top-level drawable
 * of the DraweeHierarchy. Otherwise the drawable will not be drawn.
 * <p/>
 * <p>The containing view must also call {@link #onDetach()} from its
 * {@link View#onStartTemporaryDetach()} and {@link View#onDetachedFromWindow()} methods. It must
 * call {@link #onAttach} from its {@link View#onFinishTemporaryDetach()} and
 * {@link View#onAttachedToWindow()} methods.
 */
public class DraweeHolder<DH extends DraweeHierarchy> implements VisibilityCallback {

    private boolean mIsControllerAttached = false;
    private boolean mIsHolderAttached = false;
    private boolean mIsVisible = true;
    private boolean mIsActivityStarted = true;
    private DH mHierarchy;
    private DraweeController mController = null;
    private final ActivityListener mActivityListener;
    private final DraweeEventTracker mEventTracker = new DraweeEventTracker();

    /**
     * Creates a new instance of DraweeHolder that detaches / attaches controller whenever context
     * notifies it about activity's onStop and onStart callbacks.
     * <p/>
     * <p>It is strongly recommended to pass a {@link ListenableActivity} as context. The holder will
     * then also be able to respond to onStop and onStart events from that activity, making sure the
     * image does not waste memory when the activity is stopped.
     */
    public static <DH extends DraweeHierarchy> DraweeHolder<DH> create(
            @Nullable DH hierarchy,
            Context context) {
        DraweeHolder<DH> holder = new DraweeHolder<DH>(hierarchy);
        holder.registerWithContext(context);
        return holder;
    }

    /**
     * If the given context is an instance of FbListenableActivity, then listener for its onStop and
     * onStart methods is registered that changes visibility of the holder.
     */
    public void registerWithContext(Context context) {
        // TODO(T6181423): this is not working reliably and we cannot afford photos-not-loading issues.
        //ActivityListenerManager.register(mActivityListener, context);
    }

    /**
     * Creates a new instance of DraweeHolder.
     *
     * @param hierarchy
     */
    public DraweeHolder(@Nullable DH hierarchy) {
        if (hierarchy != null) {
            setHierarchy(hierarchy);
        }
        mActivityListener = new BaseActivityListener() {
            @Override
            public void onStart(Activity activity) {
                setActivityStarted(true);
            }

            @Override
            public void onStop(Activity activity) {
                setActivityStarted(false);
            }
        };
    }

    /**
     * Gets the controller ready to display the image.
     * <p/>
     * <p>The containing view must call this method from both {@link View#onFinishTemporaryDetach()}
     * and {@link View#onAttachedToWindow()}.
     */
    public void onAttach() {
        mEventTracker.recordEvent(Event.ON_HOLDER_ATTACH);
        mIsHolderAttached = true;
        attachOrDetatchController();
    }

    /**
     * Releases resources used to display the image.
     * <p/>
     * <p>The containing view must call this method from both {@link View#onStartTemporaryDetach()}
     * and {@link View#onDetachedFromWindow()}.
     */
    public void onDetach() {
        mEventTracker.recordEvent(Event.ON_HOLDER_DETACH);
        mIsHolderAttached = false;
        attachOrDetatchController();
    }

    /**
     * Forwards the touch event to the controller.
     *
     * @param event touch event to handle
     * @return whether the event was handled or not
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mController == null) {
            return false;
        }
        return mController.onTouchEvent(event);
    }

    /**
     * Callback used to notify about top-level-drawable's visibility changes.
     */
    @Override
    public void onVisibilityChange(boolean isVisible) {
        if (mIsVisible == isVisible) {
            return;
        }
        mEventTracker.recordEvent(isVisible ? Event.ON_DRAWABLE_SHOW : Event.ON_DRAWABLE_HIDE);
        mIsVisible = isVisible;
        attachOrDetatchController();
    }

    /**
     * Callback used to notify about top-level-drawable being drawn.
     */
    @Override
    public void onDraw() {
        // draw is only expected if the controller is attached
        if (mIsControllerAttached) {
            return;
        }
        // something went wrong here; controller is not attached, yet the hierarchy has to be drawn
        // log error and attach the controller
        FLog.wtf(
                DraweeEventTracker.class,
                "%x: Draw requested for a non-attached controller %x. %s",
                System.identityHashCode(this),
                System.identityHashCode(mController),
                toString());
        mIsHolderAttached = true;
        mIsVisible = true;
        mIsActivityStarted = true;
        attachOrDetatchController();
    }

    /**
     * Sets the visibility callback to the current top-level-drawable.
     */
    private void setVisibilityCallback(@Nullable VisibilityCallback visibilityCallback) {
        Drawable drawable = getTopLevelDrawable();
        if (drawable instanceof VisibilityAwareDrawable) {
            ((VisibilityAwareDrawable) drawable).setVisibilityCallback(visibilityCallback);
        }
    }

    /**
     * Notifies the holder of activity's visibility change
     */
    private void setActivityStarted(boolean isStarted) {
        mEventTracker.recordEvent(isStarted ? Event.ON_ACTIVITY_START : Event.ON_ACTIVITY_STOP);
        mIsActivityStarted = isStarted;
        attachOrDetatchController();
    }

    /**
     * Sets a new controller.
     */
    public void setController(@Nullable DraweeController draweeController) {
        boolean wasAttached = mIsControllerAttached;
        if (wasAttached) {
            detatchController();
        }

        // Clear the old controller
        if (mController != null) {
            mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER);
            mController.setHierarchy(null);
        }
        mController = draweeController;
        if (mController != null) {
            mEventTracker.recordEvent(Event.ON_SET_CONTROLLER);
            mController.setHierarchy(mHierarchy);
        } else {
            mEventTracker.recordEvent(Event.ON_CLEAR_CONTROLLER);
        }

        if (wasAttached) {
            attachController();
        }
    }

    /**
     * Gets the controller if set, null otherwise.
     */
    @Nullable
    public DraweeController getController() {
        return mController;
    }

    /**
     * Sets the drawee hierarchy.
     */
    public void setHierarchy(DH hierarchy) {
        mEventTracker.recordEvent(Event.ON_SET_HIERARCHY);
        setVisibilityCallback(null);
        mHierarchy = Preconditions.checkNotNull(hierarchy);
        onVisibilityChange(mHierarchy.getTopLevelDrawable().isVisible());
        setVisibilityCallback(this);
        if (mController != null) {
            mController.setHierarchy(hierarchy);
        }
    }

    /**
     * Gets the drawee hierarchy if set, throws NPE otherwise.
     */
    public DH getHierarchy() {
        return Preconditions.checkNotNull(mHierarchy);
    }

    /**
     * Returns whether the hierarchy is set or not.
     */
    public boolean hasHierarchy() {
        return mHierarchy != null;
    }

    /**
     * Gets the top-level drawable if hierarchy is set, null otherwise.
     */
    public Drawable getTopLevelDrawable() {
        return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable();
    }

    private void attachController() {
        if (mIsControllerAttached) {
            return;
        }
        mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER);
        mIsControllerAttached = true;
        if (mController != null &&
                mController.getHierarchy() != null) {
            mController.onAttach();
        }
    }

    private void detatchController() {
        if (!mIsControllerAttached) {
            return;
        }
        mEventTracker.recordEvent(Event.ON_DETACH_CONTROLLER);
        mIsControllerAttached = false;
        if (mController != null) {
            mController.onDetach();
        }
    }

    private void attachOrDetatchController() {
        if (mIsHolderAttached && mIsVisible && mIsActivityStarted) {
            attachController();
        } else {
            detatchController();
        }
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                .add("controllerAttached", mIsControllerAttached)
                .add("holderAttached", mIsHolderAttached)
                .add("drawableVisible", mIsVisible)
                .add("activityStarted", mIsActivityStarted)
                .add("events", mEventTracker.toString())
                .toString();
    }
}
